跳到主要内容

Spring Cloud Security OAuth2 学习

Spring-Security-OAuth2 是什么?

Spring-Security-OAuth2 是对 OAuth2 的一种实现,并且跟我们之前学习的 Spring Security 相辅相成,与 Spring Cloud 体系的集成也非常便利

OAuth2.0 的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。

1、授权服务 (Authorization Server) 应包含对接入端以及登入用户的合法性进行验证并颁发 token 等功能,对令牌的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的 endpoints:

  • AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize
  • TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token

2、资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中 token 进行解析鉴权等,下面的过滤器用于实现 OAuth 2.0 资源服务:OAuth2AuthenticationProcessingFilter 用来对请求给出的身份令牌解析鉴权。

整个流程如下:

认证流程如下:

1、客户端请求 UAA 授权服务进行认证。 2、认证通过后由 UAA 颁发令牌。 3、客户端携带令牌 Token 请求资源服务。 4、资源服务校验令牌的合法性,合法即返回资源信息。

授权服务器配置

EnableAuthorizationServer

可以用 @EnableAuthorizationServer 注解并继承 AuthorizationServerConfigurerAdapter 来配置 OAuth2.0 授权服务器。

在 Config 包下创建 AuthorizationServer:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
//略...
}

AuthorizationServerConfigurerAdapter 要求配置以下几个类(适配器模式),这几个类是由 Spring 创建的独立的配置对象,它们会被 Spring 传入 AuthorizationServerConfigurer 中进行配置。

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

public AuthorizationServerConfigurerAdapter() {}

// AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}


// ClientDetailsServiceConfigurer 用来配置客户端详情服务(ClientDetailsService),
// 客户端详情信息在这里进行初始化,
// 你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}


// AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}

配置客户端详细信息

ClientDetailsServiceConfigurer 能够使用内存或者 JDBC 来实现客户端详情服务(ClientDetailsService),ClientDetailsService 负责查找 ClientDetails,而 ClientDetails 有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id。
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

如果使用 JDBC 的话,这个表可以在 SQL 脚本 里面找到

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService 接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

1、使用 JDBC 的方式配置如下:

//客户端详情服务,从数据库中获取
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
// 注意这个 JdbcClientDetailsService 会自动查询标准的那个表,只需按照上面给的 SQL 脚本提供就好了
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}

//用来配置客户端详情服务,客户端详情信息在这里进行初始化
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}

2、使用内存方式存储客户端详情信息,配置如下:

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.withClientDetails(clientDetailsService);
clients.inMemory()// 使用in‐memory存储
.withClient("c1")// client_id
.secret(new BCryptPasswordEncoder().encode("secret"))
// 资源列表,资源标识
.resourceIds("res1")
.authorizedGrantTypes("authorization_code",
"password","client_credentials","implicit","refresh_token")
// 该 client 允许的授权类型 authorization_code,password,refresh_token,implicit,client_credentials
// 允许的授权范围
.scopes("all")
// false跳转到授权页面,让用户点击授权,如果是true,相当于自动点击授权,就不跳转授权页面
.autoApprove(false)
// 注意!! 重定向地址只有 authorization_code 或 implicit 能使用
//加上验证回调地址
.redirectUris("http://www.baidu.com");
}

管理令牌

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。

默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。

除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了 TokenStore 接口:

InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把 "spring-jdbc" 这个依赖加入到你的 classpath 当中。

JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

1、定义 TokenConfig

在 config 包下定义 TokenConfig,我们暂时先使用 InMemoryTokenStore,生成一个普通的令牌。

@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}

2、定义 AuthorizationServerTokenServices

在 AuthorizationServer 中定义 AuthorizationServerTokenServices

@Autowired
private TokenStore tokenStore;

@Autowired
private ClientDetailsService clientDetailsService;

@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);
service.setSupportRefreshToken(true);
service.setTokenStore(tokenStore);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}

授权服务器令牌访问端点配置

AuthorizationServerEndpointsConfigurer 这个对象的实例可以完成令牌服务以及令牌 endpoint 配置。

配置授权类型(Grant Types)

AuthorizationServerEndpointsConfigurer 通过设定以下属性决定支持的授权类型(Grant Types):

authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。

userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。

authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 "authorization_code" 授权码类型模式。

implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。

tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

配置示例:

/**
* 配置授权服务器端点的属性和增强功能。
* 这里使用 JWT 作为 Token
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 用来配置令牌(token)的访问端点
endpoints
.userDetailsService(userService) // 设置用户验证服务。
// 密码授予的 AuthenticationManager,调用此方法才能支持 password 模式。
.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
.tokenStore(jwtTokenStore()) // 设置 Token 存储方式
.allowedTokenEndpointRequestMethods(HttpMethod.POST);// 允许post提交
}

配置授权端点的 URL(Endpoint URLs)

AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点 URL 链接,它有两个参数:

  • 第一个参数:String 类型的,这个端点 URL 的默认链接。
  • 第二个参数:String 类型的,你要进行替代的 URL 链接。

以上的参数都将以 "/" 字符为开始的字符串,框架的默认 URL 链接如下列表,可以作为这个 pathMapping() 方法的第一个参数:

  • /oauth/authorize:授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error:授权服务错误信息端点。
  • /oauth/check_token:用于资源服务访问的令牌解析端点。
  • /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。

需要注意的是授权端点这个 URL 应该被 Spring Security 保护起来只供授权用户访问

如果要自定义端点:

@Override
public void configure(AuthorizationServerEndpointsConfigurer oauthServer) throws Exception {
oauthServer

// Here you can override the default endpoints mappings
.pathMapping("/oauth/authorize", "/api/v1/authorize")
.pathMapping("/oauth/token", "/api/v1/token")

// .. rest of the authorization server customization
.authenticationManager(authenticationManager)
.tokenStore(tokenStore);
}

在 AuthorizationServer 配置令牌访问端点

@Autowired
private AuthorizationCodeServices authorizationCodeServices;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}

//设置授权码模式的授权码如何存取,暂时采用内存方式
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}

配置端点对应的实体

服务端最主要的一个配置就是使用 @EnableAuthorizationServer 注解,该注解的作用就是引入了一些 OAuth2 相关的端点,包含以下的端点:

AuthorizationEndpoint 根据用户认证获得授权码,有下面两个方法:

/oauth/authorize - GET
/oauth/authorize - POST

TokenEndpoint 客户端根据授权码获取 token

/oauth/token - GET
/oauth/token - POST

CheckTokenEndpoint 可以用于远程解码令牌

/oauth/check_token

WhitelabelApprovalEndpoint 显示授权服务器的确认页。

/oauth/confirm_access

WhitelabelErrorEndpoint 显示授权服务器的错误页

/oauth/error

令牌端点的安全约束

AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束,在 AuthorizationServer 中配置如下

@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("permitAll()") (1)
.checkTokenAccess("permitAll()") (2)
// .checkTokenAccess("isAuthenticated()") // 只允许验证用户访问令牌解析端点(/oauth/check_token)
.allowFormAuthenticationForClients(); (3)
}

(1)tokenkey 这个 endpoint 当使用 JwtToken 且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint 完全公开。 (2)checkToken 这个 endpoint 完全公开 (3)允许表单认证

授权服务配置总结

授权服务配置分成三大块,可以关联记忆。

1、既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。

2、既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的token。

3、既然暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等。

授权服务器 web 安全配置

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Autowired
private UserService userService;

/**
* 认证用户的来源
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}

//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}

授权码模式

下图是授权码模式交互图:

1、资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:

/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

参数列表如下:

  • client_id:客户端准入标识。
  • response_type:授权码模式固定为code。
  • scope:客户端权限。
  • redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。

2、浏览器出现向授权服务器授权页面,之后将用户同意授权。

3、授权服务器将授权码(AuthorizationCode)转经浏览器发送给 client(通过 redirect_uri)。

4、客户端拿着授权码向授权服务器索要访问 access_token,请求如下:

/uaa/oauth/token?client_id=c1&
client_secret=secret&
grant_type=authorization_code&
code=5PgfcD&
redirect_uri=http://www.baidu.com

参数列表如下

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写 authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转 url,一定和申请授权码时用的 redirect_uri 一致。

5、授权服务器返回令牌(access_token)

这种模式是四种模式中最安全的一种模式。一般用于 client 是 Web 服务器端应用或第三方的原生 App 调用资源服务的时候。因为在这种模式中 access_token 不会经过浏览器或移动端的 App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。

密码模式

下图是密码模式交互图:

(1)资源拥有者将用户名、密码发送给客户端

(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:

/uaa/oauth/token?
client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123

参数列表如下:

client_id:客户端准入标识。 # 这两个得在 base auth 里面填
client_secret:客户端秘钥。

grant_type:授权类型,填写password表示密码模式
username:资源拥有者用户名。
password:资源拥有者密码。

(3)授权服务器将令牌(access_token)发送给 client

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于 client 是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生 App 或第一方单页面应用。

资源服务器配置

@EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个 配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个 对象的实例),下面是一些可以配置的属性:

ResourceServerSecurityConfigurer 中主要包括:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • tokenStore:TokenStore类的实例,指定令牌如何访问,与tokenServices配置可选
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。

HttpSecurity 配置这个与 Spring Security 类似:

  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
  • 通过 http.authorizeRequests() 来设置受保护资源的访问规则
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链

编写 ResourceServerConfig:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

public static final String RESOURCE_ID = "res1";

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenService())
.stateless(true);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

验证token

ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题。

如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。

令牌解析方法: 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token

使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问,这在咱们授权服务配置中已经提到了,下面是一个例子,在这个例子中,我们在授权服务中配置了 /oauth/check_token/oauth/token_key 这两个端点:

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")// /oauth/token_key 安全配置
.checkTokenAccess("permitAll()") // /oauth/check_token 安全配置
}

在资源 服务配置 RemoteTokenServices ,在 ResourceServerConfig 中配置:

//资源服务令牌解析服务
@Bean
public ResourceServerTokenServices tokenService() {
//使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
RemoteTokenServices service=new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
service.setClientId("c1");
service.setClientSecret("secret");
return service;
}


@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenService())
.stateless(true);
}

Reference

Spring Cloud OAuth2 认证流程